﻿using System.Globalization;
using Nop.Core;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Payments;
using Nop.Core.Domain.Shipping;
using Nop.Data;
using Nop.Services.Catalog;
using Nop.Services.Html;
using Nop.Services.Shipping;

namespace Nop.Services.Orders;

/// <summary>
/// Order service
/// </summary>
public partial class OrderService : IOrderService
{
    #region Fields

    protected readonly IHtmlFormatter _htmlFormatter;
    protected readonly IProductService _productService;
    protected readonly IRepository<Address> _addressRepository;
    protected readonly IRepository<Customer> _customerRepository;
    protected readonly IRepository<Order> _orderRepository;
    protected readonly IRepository<OrderItem> _orderItemRepository;
    protected readonly IRepository<OrderNote> _orderNoteRepository;
    protected readonly IRepository<Product> _productRepository;
    protected readonly IRepository<ProductWarehouseInventory> _productWarehouseInventoryRepository;
    protected readonly IRepository<RecurringPayment> _recurringPaymentRepository;
    protected readonly IRepository<RecurringPaymentHistory> _recurringPaymentHistoryRepository;
    protected readonly IShipmentService _shipmentService;
    private static readonly char[] _separator = [';'];

    #endregion

    #region Ctor

    public OrderService(IHtmlFormatter htmlFormatter,
        IProductService productService,
        IRepository<Address> addressRepository,
        IRepository<Customer> customerRepository,
        IRepository<Order> orderRepository,
        IRepository<OrderItem> orderItemRepository,
        IRepository<OrderNote> orderNoteRepository,
        IRepository<Product> productRepository,
        IRepository<ProductWarehouseInventory> productWarehouseInventoryRepository,
        IRepository<RecurringPayment> recurringPaymentRepository,
        IRepository<RecurringPaymentHistory> recurringPaymentHistoryRepository,
        IShipmentService shipmentService)
    {
        _htmlFormatter = htmlFormatter;
        _productService = productService;
        _addressRepository = addressRepository;
        _customerRepository = customerRepository;
        _orderRepository = orderRepository;
        _orderItemRepository = orderItemRepository;
        _orderNoteRepository = orderNoteRepository;
        _productRepository = productRepository;
        _productWarehouseInventoryRepository = productWarehouseInventoryRepository;
        _recurringPaymentRepository = recurringPaymentRepository;
        _recurringPaymentHistoryRepository = recurringPaymentHistoryRepository;
        _shipmentService = shipmentService;
    }

    #endregion

    #region Utilities

    /// <summary>
    /// Gets the value indicating whether there are shipment items with a positive quantity in order shipments.
    /// </summary>
    /// <param name="order">Order</param>
    /// <param name="predicate">Predicate to filter shipments or null to check all shipments</param>
    /// <returns>The <see cref="Task"/> containing the value indicating whether there are shipment items with a positive quantity in order shipments.</returns>
    protected virtual async Task<bool> HasShipmentItemsAsync(Order order, Func<Shipment, bool> predicate = null)
    {
        var shipments = await _shipmentService.GetShipmentsByOrderIdAsync(order.Id);
        if (shipments?.Any(shipment => predicate == null || predicate(shipment)) == true)
        {
            var orderItems = await GetOrderItemsAsync(order.Id, isShipEnabled: true);
            if (orderItems?.Any() == true)
            {
                foreach (var shipment in shipments)
                {
                    if (predicate?.Invoke(shipment) == false)
                        continue;

                    bool hasPositiveQuantity(ShipmentItem shipmentItem)
                    {
                        return orderItems.Any(orderItem => orderItem.Id == shipmentItem.OrderItemId && shipmentItem.Quantity > 0);
                    }

                    var shipmentItems = await _shipmentService.GetShipmentItemsByShipmentIdAsync(shipment.Id);
                    if (shipmentItems?.Any(hasPositiveQuantity) == true)
                        return true;
                }
            }
        }

        return false;
    }

    #endregion

    #region Methods

    #region Orders

    /// <summary>
    /// Gets an order
    /// </summary>
    /// <param name="orderId">The order identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the order
    /// </returns>
    public virtual async Task<Order> GetOrderByIdAsync(int orderId)
    {
        return await _orderRepository.GetByIdAsync(orderId, cache => default, useShortTermCache: true);
    }

    /// <summary>
    /// Gets an order
    /// </summary>
    /// <param name="customOrderNumber">The custom order number</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the order
    /// </returns>
    public virtual async Task<Order> GetOrderByCustomOrderNumberAsync(string customOrderNumber)
    {
        if (string.IsNullOrEmpty(customOrderNumber))
            return null;

        return await _orderRepository.Table
            .FirstOrDefaultAsync(o => o.CustomOrderNumber == customOrderNumber);
    }

    /// <summary>
    /// Gets an order by order item identifier
    /// </summary>
    /// <param name="orderItemId">The order item identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the order
    /// </returns>
    public virtual async Task<Order> GetOrderByOrderItemAsync(int orderItemId)
    {
        if (orderItemId == 0)
            return null;

        return await (from o in _orderRepository.Table
            join oi in _orderItemRepository.Table on o.Id equals oi.OrderId
            where oi.Id == orderItemId
            select o).FirstOrDefaultAsync();
    }

    /// <summary>
    /// Get orders by identifiers
    /// </summary>
    /// <param name="orderIds">Order identifiers</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the order
    /// </returns>
    public virtual async Task<IList<Order>> GetOrdersByIdsAsync(int[] orderIds)
    {
        return await _orderRepository.GetByIdsAsync(orderIds, includeDeleted: false);
    }

    /// <summary>
    /// Get orders by guids
    /// </summary>
    /// <param name="orderGuids">Order guids</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the orders
    /// </returns>
    public virtual async Task<IList<Order>> GetOrdersByGuidsAsync(Guid[] orderGuids)
    {
        if (orderGuids == null)
            return null;

        var query = from o in _orderRepository.Table
            where orderGuids.Contains(o.OrderGuid)
            select o;
        var orders = await query.ToListAsync();

        return orders;
    }

    /// <summary>
    /// Gets an order
    /// </summary>
    /// <param name="orderGuid">The order identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the order
    /// </returns>
    public virtual async Task<Order> GetOrderByGuidAsync(Guid orderGuid)
    {
        if (orderGuid == Guid.Empty)
            return null;

        var query = from o in _orderRepository.Table
            where o.OrderGuid == orderGuid
            select o;
        var order = await query.FirstOrDefaultAsync();

        return order;
    }

    /// <summary>
    /// Deletes an order
    /// </summary>
    /// <param name="order">The order</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeleteOrderAsync(Order order)
    {
        await _orderRepository.DeleteAsync(order);
    }

    /// <summary>
    /// Search orders
    /// </summary>
    /// <param name="storeId">Store identifier; 0 to load all orders</param>
    /// <param name="vendorId">Vendor identifier; null to load all orders</param>
    /// <param name="customerId">Customer identifier; 0 to load all orders</param>
    /// <param name="productId">Product identifier which was purchased in an order; 0 to load all orders</param>
    /// <param name="affiliateId">Affiliate identifier; 0 to load all orders</param>
    /// <param name="billingCountryId">Billing country identifier; 0 to load all orders</param>
    /// <param name="warehouseId">Warehouse identifier, only orders with products from a specified warehouse will be loaded; 0 to load all orders</param>
    /// <param name="paymentMethodSystemName">Payment method system name; null to load all records</param>
    /// <param name="createdFromUtc">Created date from (UTC); null to load all records</param>
    /// <param name="createdToUtc">Created date to (UTC); null to load all records</param>
    /// <param name="osIds">Order status identifiers; null to load all orders</param>
    /// <param name="psIds">Payment status identifiers; null to load all orders</param>
    /// <param name="ssIds">Shipping status identifiers; null to load all orders</param>
    /// <param name="billingPhone">Billing phone. Leave empty to load all records.</param>
    /// <param name="billingEmail">Billing email. Leave empty to load all records.</param>
    /// <param name="billingLastName">Billing last name. Leave empty to load all records.</param>
    /// <param name="orderNotes">Search in order notes. Leave empty to load all records.</param>
    /// <param name="pageIndex">Page index</param>
    /// <param name="pageSize">Page size</param>
    /// <param name="getOnlyTotalCount">A value in indicating whether you want to load only total number of records. Set to "true" if you don't want to load data from database</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the orders
    /// </returns>
    public virtual async Task<IPagedList<Order>> SearchOrdersAsync(int storeId = 0,
        int vendorId = 0, int customerId = 0,
        int productId = 0, int affiliateId = 0, int warehouseId = 0,
        int billingCountryId = 0, string paymentMethodSystemName = null,
        DateTime? createdFromUtc = null, DateTime? createdToUtc = null,
        List<int> osIds = null, List<int> psIds = null, List<int> ssIds = null,
        string billingPhone = null, string billingEmail = null, string billingLastName = "",
        string orderNotes = null, int pageIndex = 0, int pageSize = int.MaxValue, bool getOnlyTotalCount = false)
    {
        var query = _orderRepository.Table;

        if (storeId > 0)
            query = query.Where(o => o.StoreId == storeId);

        if (vendorId > 0)
        {
            query = from o in query
                join oi in _orderItemRepository.Table on o.Id equals oi.OrderId
                join p in _productRepository.Table on oi.ProductId equals p.Id
                where p.VendorId == vendorId
                select o;

            query = query.Distinct();
        }

        if (customerId > 0)
            query = query.Where(o => o.CustomerId == customerId);

        if (productId > 0)
        {
            query = from o in query
                join oi in _orderItemRepository.Table on o.Id equals oi.OrderId
                where oi.ProductId == productId
                select o;

            query = query.Distinct();
        }

        if (warehouseId > 0)
        {
            var manageStockInventoryMethodId = (int)ManageInventoryMethod.ManageStock;

            query = from o in query
                join oi in _orderItemRepository.Table on o.Id equals oi.OrderId
                join p in _productRepository.Table on oi.ProductId equals p.Id
                join pwi in _productWarehouseInventoryRepository.Table on p.Id equals pwi.ProductId into ps
                from pwi in ps.DefaultIfEmpty()
                where
                    //"Use multiple warehouses" enabled
                    //we search in each warehouse
                    (p.ManageInventoryMethodId == manageStockInventoryMethodId && p.UseMultipleWarehouses && pwi.WarehouseId == warehouseId) ||
                    //"Use multiple warehouses" disabled
                    //we use standard "warehouse" property
                    ((p.ManageInventoryMethodId != manageStockInventoryMethodId || !p.UseMultipleWarehouses) && p.WarehouseId == warehouseId)
                select o;

            query = query.Distinct();
        }

        if (!string.IsNullOrEmpty(paymentMethodSystemName))
            query = query.Where(o => o.PaymentMethodSystemName == paymentMethodSystemName);

        if (affiliateId > 0)
            query = query.Where(o => o.AffiliateId == affiliateId);

        if (createdFromUtc.HasValue)
            query = query.Where(o => createdFromUtc.Value <= o.CreatedOnUtc);

        if (createdToUtc.HasValue)
            query = query.Where(o => createdToUtc.Value >= o.CreatedOnUtc);

        if (osIds != null && osIds.Any())
            query = query.Where(o => osIds.Contains(o.OrderStatusId));

        if (psIds != null && psIds.Any())
            query = query.Where(o => psIds.Contains(o.PaymentStatusId));

        if (ssIds != null && ssIds.Any())
            query = query.Where(o => ssIds.Contains(o.ShippingStatusId));

        if (!string.IsNullOrEmpty(orderNotes))
            query = query.Where(o => _orderNoteRepository.Table.Any(oNote => oNote.OrderId == o.Id && oNote.Note.Contains(orderNotes)));

        query = from o in query
            join oba in _addressRepository.Table on o.BillingAddressId equals oba.Id
            where
                (billingCountryId <= 0 || (oba.CountryId == billingCountryId)) &&
                (string.IsNullOrEmpty(billingPhone) || (!string.IsNullOrEmpty(oba.PhoneNumber) && oba.PhoneNumber.Contains(billingPhone))) &&
                (string.IsNullOrEmpty(billingEmail) || (!string.IsNullOrEmpty(oba.Email) && oba.Email.Contains(billingEmail))) &&
                (string.IsNullOrEmpty(billingLastName) || (!string.IsNullOrEmpty(oba.LastName) && oba.LastName.Contains(billingLastName)))
            select o;

        query = query.Where(o => !o.Deleted);
        query = query.OrderByDescending(o => o.CreatedOnUtc);

        //database layer paging
        return await query.ToPagedListAsync(pageIndex, pageSize, getOnlyTotalCount);
    }

    /// <summary>
    /// Inserts an order
    /// </summary>
    /// <param name="order">Order</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertOrderAsync(Order order)
    {
        await _orderRepository.InsertAsync(order);
    }

    /// <summary>
    /// Updates the order
    /// </summary>
    /// <param name="order">The order</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task UpdateOrderAsync(Order order)
    {
        await _orderRepository.UpdateAsync(order);
    }

    /// <summary>
    /// Parse tax rates
    /// </summary>
    /// <param name="order">Order</param>
    /// <param name="taxRatesStr"></param>
    /// <returns>Rates</returns>
    public virtual SortedDictionary<decimal, decimal> ParseTaxRates(Order order, string taxRatesStr)
    {
        var taxRatesDictionary = new SortedDictionary<decimal, decimal>();

        if (string.IsNullOrEmpty(taxRatesStr))
            return taxRatesDictionary;

        var lines = taxRatesStr.Split(_separator, StringSplitOptions.RemoveEmptyEntries);
        foreach (var line in lines)
        {
            if (string.IsNullOrEmpty(line.Trim()))
                continue;

            var taxes = line.Split(':');
            if (taxes.Length != 2)
                continue;

            try
            {
                var taxRate = decimal.Parse(taxes[0].Trim(), CultureInfo.InvariantCulture);
                var taxValue = decimal.Parse(taxes[1].Trim(), CultureInfo.InvariantCulture);
                taxRatesDictionary.Add(taxRate, taxValue);
            }
            catch
            {
                // ignored
            }
        }

        //add at least one tax rate (0%)
        if (!taxRatesDictionary.Any())
            taxRatesDictionary.Add(decimal.Zero, decimal.Zero);

        return taxRatesDictionary;
    }

    /// <summary>
    /// Gets a value indicating whether an order has items to be added to a shipment
    /// </summary>
    /// <param name="order">Order</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains a value indicating whether an order has items to be added to a shipment
    /// </returns>
    public virtual async Task<bool> HasItemsToAddToShipmentAsync(Order order)
    {
        ArgumentNullException.ThrowIfNull(order);

        foreach (var orderItem in await GetOrderItemsAsync(order.Id, isShipEnabled: true)) //we can ship only shippable products
        {
            var totalNumberOfItemsCanBeAddedToShipment = await GetTotalNumberOfItemsCanBeAddedToShipmentAsync(orderItem);
            if (totalNumberOfItemsCanBeAddedToShipment <= 0)
                continue;

            //yes, we have at least one item to create a new shipment
            return true;
        }

        return false;
    }

    /// <summary>
    /// Gets a value indicating whether an order has items to ship
    /// </summary>
    /// <param name="order">Order</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains a value indicating whether an order has items to ship
    /// </returns>
    public virtual async Task<bool> HasItemsToShipAsync(Order order)
    {
        ArgumentNullException.ThrowIfNull(order);

        if (order.PickupInStore)
            return false;

        return await HasShipmentItemsAsync(order, shipment => !shipment.ShippedDateUtc.HasValue);
    }

    /// <summary>
    /// Gets a value indicating whether there are shipment items to mark as 'ready for pickup' in order shipments.
    /// </summary>
    /// <param name="order">Order</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains a value indicating whether there are shipment items to mark as 'ready for pickup' in order shipments.
    /// </returns>
    public virtual async Task<bool> HasItemsToReadyForPickupAsync(Order order)
    {
        ArgumentNullException.ThrowIfNull(order);

        if (!order.PickupInStore)
            return false;

        return await HasShipmentItemsAsync(order, shipment => !shipment.ReadyForPickupDateUtc.HasValue);
    }

    /// <summary>
    /// Gets a value indicating whether an order has items to deliver
    /// </summary>
    /// <param name="order">Order</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains a value indicating whether an order has items to deliver
    /// </returns>
    public virtual async Task<bool> HasItemsToDeliverAsync(Order order)
    {
        ArgumentNullException.ThrowIfNull(order);

        return await HasShipmentItemsAsync(order, shipment => (shipment.ShippedDateUtc.HasValue || shipment.ReadyForPickupDateUtc.HasValue) && !shipment.DeliveryDateUtc.HasValue);
    }

    #endregion

    #region Orders items

    /// <summary>
    /// Gets an order item
    /// </summary>
    /// <param name="orderItemId">Order item identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the order item
    /// </returns>
    public virtual async Task<OrderItem> GetOrderItemByIdAsync(int orderItemId)
    {
        return await _orderItemRepository.GetByIdAsync(orderItemId, cache => default, useShortTermCache: true);
    }

    /// <summary>
    /// Gets a product of specify order item
    /// </summary>
    /// <param name="orderItemId">Order item identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product
    /// </returns>
    public virtual async Task<Product> GetProductByOrderItemIdAsync(int orderItemId)
    {
        if (orderItemId == 0)
            return null;

        return await (from p in _productRepository.Table
            join oi in _orderItemRepository.Table on p.Id equals oi.ProductId
            where oi.Id == orderItemId
            select p).SingleOrDefaultAsync();
    }

    /// <summary>
    /// Gets a list items of order
    /// </summary>
    /// <param name="orderId">Order identifier</param>
    /// <param name="isNotReturnable">Value indicating whether this product is returnable; pass null to ignore</param>
    /// <param name="isShipEnabled">Value indicating whether the entity is ship enabled; pass null to ignore</param>
    /// <param name="vendorId">Vendor identifier; pass 0 to ignore</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<IList<OrderItem>> GetOrderItemsAsync(int orderId, bool? isNotReturnable = null, bool? isShipEnabled = null, int vendorId = 0)
    {
        if (orderId == 0)
            return new List<OrderItem>();

        return await (from oi in _orderItemRepository.Table
            join p in _productRepository.Table on oi.ProductId equals p.Id
            where
                oi.OrderId == orderId &&
                (!isShipEnabled.HasValue || (p.IsShipEnabled == isShipEnabled.Value)) &&
                (!isNotReturnable.HasValue || (p.NotReturnable == isNotReturnable)) &&
                (vendorId <= 0 || (p.VendorId == vendorId))
            select oi).ToListAsync();
    }

    /// <summary>
    /// Gets an item
    /// </summary>
    /// <param name="orderItemGuid">Order identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the order item
    /// </returns>
    public virtual async Task<OrderItem> GetOrderItemByGuidAsync(Guid orderItemGuid)
    {
        if (orderItemGuid == Guid.Empty)
            return null;

        var query = from orderItem in _orderItemRepository.Table
            where orderItem.OrderItemGuid == orderItemGuid
            select orderItem;
        var item = await query.FirstOrDefaultAsync();
        return item;
    }

    /// <summary>
    /// Gets all downloadable order items
    /// </summary>
    /// <param name="customerId">Customer identifier; null to load all records</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the order items
    /// </returns>
    public virtual async Task<IList<OrderItem>> GetDownloadableOrderItemsAsync(int customerId)
    {
        if (customerId == 0)
            throw new ArgumentOutOfRangeException(nameof(customerId));

        var query = from orderItem in _orderItemRepository.Table
            join o in _orderRepository.Table on orderItem.OrderId equals o.Id
            join p in _productRepository.Table on orderItem.ProductId equals p.Id
            where customerId == o.CustomerId &&
                  p.IsDownload &&
                  !o.Deleted
            orderby o.CreatedOnUtc descending, orderItem.Id
            select orderItem;

        var orderItems = await query.ToListAsync();
        return orderItems;
    }

    /// <summary>
    /// Delete an order item
    /// </summary>
    /// <param name="orderItem">The order item</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeleteOrderItemAsync(OrderItem orderItem)
    {
        await _orderItemRepository.DeleteAsync(orderItem);
    }

    /// <summary>
    /// Gets a total number of items in all shipments
    /// </summary>
    /// <param name="orderItem">Order item</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the total number of items in all shipments
    /// </returns>
    public virtual async Task<int> GetTotalNumberOfItemsInAllShipmentsAsync(OrderItem orderItem)
    {
        ArgumentNullException.ThrowIfNull(orderItem);

        var totalInShipments = 0;
        var shipments = await _shipmentService.GetShipmentsByOrderIdAsync(orderItem.OrderId);

        for (var i = 0; i < shipments.Count; i++)
        {
            var shipment = shipments[i];
            var si = (await _shipmentService.GetShipmentItemsByShipmentIdAsync(shipment.Id))
                .FirstOrDefault(x => x.OrderItemId == orderItem.Id);
            if (si != null)
            {
                totalInShipments += si.Quantity;
            }
        }

        return totalInShipments;
    }

    /// <summary>
    /// Gets a total number of already items which can be added to new shipments
    /// </summary>
    /// <param name="orderItem">Order item</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the total number of already delivered items which can be added to new shipments
    /// </returns>
    public virtual async Task<int> GetTotalNumberOfItemsCanBeAddedToShipmentAsync(OrderItem orderItem)
    {
        ArgumentNullException.ThrowIfNull(orderItem);

        var totalInShipments = await GetTotalNumberOfItemsInAllShipmentsAsync(orderItem);

        var qtyOrdered = orderItem.Quantity;
        var qtyCanBeAddedToShipmentTotal = qtyOrdered - totalInShipments;
        if (qtyCanBeAddedToShipmentTotal < 0)
            qtyCanBeAddedToShipmentTotal = 0;

        return qtyCanBeAddedToShipmentTotal;
    }

    /// <summary>
    /// Gets a value indicating whether download is allowed
    /// </summary>
    /// <param name="orderItem">Order item to check</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the true if download is allowed; otherwise, false.
    /// </returns>
    public virtual async Task<bool> IsDownloadAllowedAsync(OrderItem orderItem)
    {
        if (orderItem is null)
            return false;

        var order = await GetOrderByIdAsync(orderItem.OrderId);
        if (order == null || order.Deleted)
            return false;

        //order status
        if (order.OrderStatus == OrderStatus.Cancelled)
            return false;

        var product = await _productService.GetProductByIdAsync(orderItem.ProductId);

        if (product == null || !product.IsDownload)
            return false;

        //payment status
        switch (product.DownloadActivationType)
        {
            case DownloadActivationType.WhenOrderIsPaid:
                if (order.PaymentStatus == PaymentStatus.Paid && order.PaidDateUtc.HasValue)
                {
                    //expiration date
                    if (product.DownloadExpirationDays.HasValue)
                    {
                        if (order.PaidDateUtc.Value.AddDays(product.DownloadExpirationDays.Value) > DateTime.UtcNow)
                        {
                            return true;
                        }
                    }
                    else
                    {
                        return true;
                    }
                }

                break;
            case DownloadActivationType.Manually:
                if (orderItem.IsDownloadActivated)
                {
                    //expiration date
                    if (product.DownloadExpirationDays.HasValue)
                    {
                        if (order.CreatedOnUtc.AddDays(product.DownloadExpirationDays.Value) > DateTime.UtcNow)
                        {
                            return true;
                        }
                    }
                    else
                    {
                        return true;
                    }
                }

                break;
            default:
                break;
        }

        return false;
    }

    /// <summary>
    /// Gets a value indicating whether license download is allowed
    /// </summary>
    /// <param name="orderItem">Order item to check</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the true if license download is allowed; otherwise, false.
    /// </returns>
    public virtual async Task<bool> IsLicenseDownloadAllowedAsync(OrderItem orderItem)
    {
        if (orderItem == null)
            return false;

        return await IsDownloadAllowedAsync(orderItem) &&
               orderItem.LicenseDownloadId.HasValue &&
               orderItem.LicenseDownloadId > 0;
    }

    /// <summary>
    /// Inserts a order item
    /// </summary>
    /// <param name="orderItem">Order item</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertOrderItemAsync(OrderItem orderItem)
    {
        await _orderItemRepository.InsertAsync(orderItem);
    }

    /// <summary>
    /// Updates a order item
    /// </summary>
    /// <param name="orderItem">Order item</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task UpdateOrderItemAsync(OrderItem orderItem)
    {
        await _orderItemRepository.UpdateAsync(orderItem);
    }

    #endregion

    #region Orders notes

    /// <summary>
    /// Gets an order note
    /// </summary>
    /// <param name="orderNoteId">The order note identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the order note
    /// </returns>
    public virtual async Task<OrderNote> GetOrderNoteByIdAsync(int orderNoteId)
    {
        return await _orderNoteRepository.GetByIdAsync(orderNoteId);
    }

    /// <summary>
    /// Gets a list notes of order
    /// </summary>
    /// <param name="orderId">Order identifier</param>
    /// <param name="displayToCustomer">Value indicating whether a customer can see a note; pass null to ignore</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<IList<OrderNote>> GetOrderNotesByOrderIdAsync(int orderId, bool? displayToCustomer = null)
    {
        if (orderId == 0)
            return new List<OrderNote>();

        var query = _orderNoteRepository.Table.Where(on => on.OrderId == orderId);

        if (displayToCustomer.HasValue)
        {
            query = query.Where(on => on.DisplayToCustomer == displayToCustomer);
        }

        return await query.ToListAsync();
    }

    /// <summary>
    /// Deletes an order note
    /// </summary>
    /// <param name="orderNote">The order note</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeleteOrderNoteAsync(OrderNote orderNote)
    {
        await _orderNoteRepository.DeleteAsync(orderNote);
    }

    /// <summary>
    /// Formats the order note text
    /// </summary>
    /// <param name="orderNote">Order note</param>
    /// <returns>Formatted text</returns>
    public virtual string FormatOrderNoteText(OrderNote orderNote)
    {
        ArgumentNullException.ThrowIfNull(orderNote);

        var text = orderNote.Note;

        if (string.IsNullOrEmpty(text))
            return string.Empty;

        text = _htmlFormatter.FormatText(text, false, true, false, false, false, false);

        return text;
    }

    /// <summary>
    /// Inserts an order note
    /// </summary>
    /// <param name="orderNote">The order note</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertOrderNoteAsync(OrderNote orderNote)
    {
        await _orderNoteRepository.InsertAsync(orderNote);
    }

    #endregion

    #region Recurring payments

    /// <summary>
    /// Deletes a recurring payment
    /// </summary>
    /// <param name="recurringPayment">Recurring payment</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeleteRecurringPaymentAsync(RecurringPayment recurringPayment)
    {
        await _recurringPaymentRepository.DeleteAsync(recurringPayment);
    }

    /// <summary>
    /// Gets a recurring payment
    /// </summary>
    /// <param name="recurringPaymentId">The recurring payment identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the recurring payment
    /// </returns>
    public virtual async Task<RecurringPayment> GetRecurringPaymentByIdAsync(int recurringPaymentId)
    {
        return await _recurringPaymentRepository.GetByIdAsync(recurringPaymentId, cache => default);
    }

    /// <summary>
    /// Inserts a recurring payment
    /// </summary>
    /// <param name="recurringPayment">Recurring payment</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertRecurringPaymentAsync(RecurringPayment recurringPayment)
    {
        await _recurringPaymentRepository.InsertAsync(recurringPayment);
    }

    /// <summary>
    /// Updates the recurring payment
    /// </summary>
    /// <param name="recurringPayment">Recurring payment</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task UpdateRecurringPaymentAsync(RecurringPayment recurringPayment)
    {
        await _recurringPaymentRepository.UpdateAsync(recurringPayment);
    }

    /// <summary>
    /// Search recurring payments
    /// </summary>
    /// <param name="storeId">The store identifier; 0 to load all records</param>
    /// <param name="customerId">The customer identifier; 0 to load all records</param>
    /// <param name="initialOrderId">The initial order identifier; 0 to load all records</param>
    /// <param name="initialOrderStatus">Initial order status identifier; null to load all records</param>
    /// <param name="pageIndex">Page index</param>
    /// <param name="pageSize">Page size</param>
    /// <param name="showHidden">A value indicating whether to show hidden records</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the recurring payments
    /// </returns>
    public virtual async Task<IPagedList<RecurringPayment>> SearchRecurringPaymentsAsync(int storeId = 0,
        int customerId = 0, int initialOrderId = 0, OrderStatus? initialOrderStatus = null,
        int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false)
    {
        int? initialOrderStatusId = null;
        if (initialOrderStatus.HasValue)
            initialOrderStatusId = (int)initialOrderStatus.Value;

        var query1 = from rp in _recurringPaymentRepository.Table
            join o in _orderRepository.Table on rp.InitialOrderId equals o.Id
            join c in _customerRepository.Table on o.CustomerId equals c.Id
            where
                !rp.Deleted &&
                (showHidden || !o.Deleted) &&
                (showHidden || !c.Deleted) &&
                (showHidden || rp.IsActive) &&
                (customerId == 0 || o.CustomerId == customerId) &&
                (storeId == 0 || o.StoreId == storeId) &&
                (initialOrderId == 0 || o.Id == initialOrderId) &&
                (!initialOrderStatusId.HasValue || initialOrderStatusId.Value == 0 ||
                 o.OrderStatusId == initialOrderStatusId.Value)
            select rp.Id;

        var query2 = from rp in _recurringPaymentRepository.Table
            where query1.Contains(rp.Id)
            orderby rp.StartDateUtc, rp.Id
            select rp;

        var recurringPayments = await query2.ToPagedListAsync(pageIndex, pageSize);

        return recurringPayments;
    }

    #endregion

    #region Recurring payments history

    /// <summary>
    /// Gets a recurring payment history
    /// </summary>
    /// <param name="recurringPayment">The recurring payment</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task<IList<RecurringPaymentHistory>> GetRecurringPaymentHistoryAsync(RecurringPayment recurringPayment)
    {
        ArgumentNullException.ThrowIfNull(recurringPayment);

        return await _recurringPaymentHistoryRepository.Table
            .Where(rph => rph.RecurringPaymentId == recurringPayment.Id)
            .ToListAsync();
    }

    /// <summary>
    /// Inserts a recurring payment history entry
    /// </summary>
    /// <param name="recurringPaymentHistory">Recurring payment history entry</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertRecurringPaymentHistoryAsync(RecurringPaymentHistory recurringPaymentHistory)
    {
        await _recurringPaymentHistoryRepository.InsertAsync(recurringPaymentHistory);
    }

    #endregion

    #endregion
}